/**
 * Copyright 2009 Humboldt University of Berlin, INRIA.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *
 */
package de.hu_berlin.german.korpling.saltnpepper.misc.util;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import org.eclipse.emf.common.util.BasicEList;
import org.eclipse.emf.common.util.EList;

/**
 * The class {@link HashEList} provides an object derived from the type {@link EList} and additionally implements a HashMap
 * in which each object will be stored too, with the object as key and the object count as value. This enables to check whether and how often
 * the list object already contains a given object via the {@link HashEList#contains(Object)} and {@link HashEList#containsAll(Collection)}
 * methods. Nevertheless, the adding and deletion of an object into or from the list will take some more time, for keeping the HashMap up-to-date.    
 * 
 * The {@link HashEList} class is just a container class and delegates the call of the methods to another {@link EList} object, which 
 * was given when initializing the {@link HashEList} object in its constructor. If no object is given in the constructor, an object of
 * type {@link BasicEList}} will be created and the methods will be delegated to that object.
 * 
 * @author Florian Zipser, hildebax
 *
 * @param <E> The class of the elements in the list
 
 */
public class HashEList<E> implements EList<E>
{
	
	/**
	 * The default initial capacity for the internal HashMap.
	 */
	private static int defaultInitialCapacity = 10000;
	
	/**
	 * The origin {@link EList} object, to which all methods will be delegated to.
	 */
	private EList<E> originEList = null;
	
	/**
	 * The HashMap 
	 */
	private HashMap<E,Integer> hashMap = null; 
	
	/**
	 * Initializes a new object of type {@link HashEList} and creates a new {@link BasicEList} object for setting it to the 
	 * origin {@link EList} object, to which all methods will be delegated to. Also initializes the internal HashMap with the
	 * default initial capacity.  
	 */
	public HashEList()
	{
		this(defaultInitialCapacity);
	}
	
	/**
	 * Initializes a new object of type {@link HashEList} and creates a new {@link BasicEList} object for setting it to the 
	 * origin {@link EList} object, to which all methods will be delegated to. Also initializes the internal HashMap with the
	 * given initial capacity.
	 * 
	 * @param initialCapacity The initial capacity for the internal HashMap
	 */
	public HashEList(int initialCapacity) {
		originEList = new BasicEList<E>();
		hashMap     = new HashMap<E,Integer>(initialCapacity);
	}
	
	/**
	 * Initializes a new object of type {@link HashEList} and sets the origin {@link EList} object to the given one. All methods
	 * will be delegated to the given object. Also initializes the internal HashMap with the default initial capacity.
	 *
	 * @param originEList origin {@link EList} object to which all methods will be delegated to
	 */
	public HashEList(EList<E> originEList)
	{
		this(originEList, defaultInitialCapacity);
	}

	/**
	 * Initializes a new object of type {@link HashEList} and sets the origin {@link EList} object to the given one. All methods
	 * will be delegated to the given object. Also initializes the internal HashMap with the default initial capacity.
	 * 
	 * @param originEList origin {@link EList} object to which all methods will be delegated to
	 * @param initialCapacity The initial capacity for the internal HashMap
	 */
	public HashEList(EList<E> originEList, int initialCapacity)
	{
		this.originEList = originEList;
		hashMap          = new HashMap<E,Integer>(initialCapacity);
		for (E e : originEList) {
			hashAdd(e);
		}
	}
	
	/**
	 * Returns the origin {@link EList} object to which all methods will be delegated to
	 * @return originEList The origin EList object
	 */
	public EList<E> getOriginEList() {
		return originEList;
	}
	
	
	/**
	 * Returns true if the given object o is contained in this HashEList  
	 * @param o the object
	 * @return whether the object is contained
	 */
	public boolean contains(Object o) {
		return hashMap.containsKey(o);
	}

	/**
	 * Returns the number of occurences of the given object o in this HashEList
	 * @param o the object
	 * @return the number of occurences 
	 */
	public int numOfOccurences(Object o) {
		if (hashMap.containsKey(o)) return hashMap.get(o);
		return 0;
	}

	/**
	 * Adds the given instance of E to this HashEList 
	 * @param e the instance of E to add
	 * @return whether the operation was successful
	 */
	public boolean add(E e) {
		if (originEList.add(e)) {
			hashAdd(e);
			return true;
		}
		return false;
	}

	/**
	 * Adds the given instance of E to the internal HashMap
	 * If given parameter e is not contained yet, it is added paired with it's number of occurences, i.e. 1.
	 * If e is already contained, the number of occurrences of e is increased.
	 * @param e the instance of E to add
	 */
	private void hashAdd(E e) {
		switch (numOfOccurences(e)) {
			case  0: hashMap.put(e, 1); break;
			default: hashMap.put(e, hashMap.get(e)+1);
		}
	}
	
	/**
	 * Removes all occurrences of the given object from this HashEList, if it is contained
	 * @param o the object to remove
	 * @return whether the object has been removed 
	 */
	public boolean remove(Object o) {
		if (originEList.remove(o)) {
			hashRemove(o);
			return true;
		}
		return false;
	}

	/**
	 * Removes all occurrences of the given object from the internal HashMap, if it is contained
	 * @param o the object to remove
	 */
	private void hashRemove(Object o) {
		switch (numOfOccurences(o)) {
			case  0: /*nothing to do*/ break; 
			default: hashMap.remove(o); 
		}
	}
	
	/**
	 * Removes one occurrence of the given object from the internal HashMap, if it is contained
	 * @param o the object to remove
	 */
	@SuppressWarnings("unchecked")
	private void hashRemoveOne(Object o) {
		switch (numOfOccurences(o)) {
			case  0:                    break; 
			case  1: hashMap.remove(o); break;
			default: hashMap.put((E)o, hashMap.get(o)-1); 
		}
	}
	
	/**
	 * Returns true if this HashEList contains all of the elements of the given collection
	 * @param c the collection
	 * @return whether all elements of c are contained
	 */
	public boolean containsAll(Collection<?> c) {
		for (Object o : c) {
			if (!contains(o)) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Adds all elements of the given collection to this HashEList
	 * @param c the collection containing the elements to add
	 * @return whether the operation was successful 
	 */
	public boolean addAll(Collection<? extends E> c) {
		if (originEList.addAll(c)) {
			hashAddAll(c);
			return true;
		}
		return false;
	}

	/**
	 * Inserts all of the elements of the given collection into this HashEList at the given index
	 * @param index index at which to insert
	 * @param c the collection containing the elements to add  
	 * @return whether the operation was successful 
	 */
	public boolean addAll(int index, Collection<? extends E> c) {
		if (originEList.addAll(index,c)) {
			hashAddAll(c);
			return true;
		}
		return false;
	}
	
	/**
	 * Adds all of the elements of the given collection to the internal HashMap 
	 * @param c the collection containing the elements to add
	 */
	@SuppressWarnings("unchecked")
	private void hashAddAll(Collection<? extends E> c) {
		for (Object o : c) {
			hashAdd((E)o);
		}
	}
	
	/**
	 * Removes all occurrences of all elements of the given collection from this HashEList
	 * @param c the collection containing the elements to add
	 * @return whether the operation was successful
	 */
	@SuppressWarnings("unchecked")
	public boolean removeAll(Collection<?> c) {
		if (originEList.removeAll(c)) {
			for (Object o : c) {
				hashRemove((E)o);
			}
			return true;			
		}
		return false;
	}
	
	/**
	 * Removes all occurrences of all elements in the given collection from this HashEList
	 * @param c the collection containing the elements to retain 
	 * @return whether the operation was successful 
	 */
	public boolean retainAll(Collection<?> c) {
		if (originEList.retainAll(c)) {
			for (Iterator<E> keyIter = hashMap.keySet().iterator(); keyIter.hasNext();) {
				if (!c.contains(keyIter.next())) {
					keyIter.remove();
				}
			}
			return true;
		}
		return false;
	}
	
	/**
	 * Removes all occurrences of elements from this HashEList 
	 */
	public void clear() {
		originEList.clear();
		hashMap.clear();
	}

	/**
	 * Replaces the element at the given index in this HashEList with the given element  
	 * @param index index of the element to replace
	 * @param element element to be inserted at index
	 * @return the element previously at the given index
	 */
	public E set(int index, E element) {
		E retVal = originEList.set(index, element);
		hashAdd(element);
		hashRemoveOne(retVal);
		return retVal;
	}
	
	/**
	 * Inserts the given element at the given index in this HashEList. Any contents at or subsequent to the index are shifted to the right 
	 * @param index index at which the element is to be inserted
	 * @param element element to be inserted
	 */
	public void add(int index, E element) {
		originEList.add(index, element);
		hashAdd(element);
	}
	
	/**
	 * Removes the element at the given index in this HashEList. Any contents subsequent to the index are shifted to the left 
	 * @param index index at which an element is to be removed 
	 * @return the element that was removed
	 */
	public E remove(int index) {
		E retVal = originEList.remove(index);
		hashRemoveOne(retVal);
		return retVal;
	}

	
//######################
//  delegation methods
//######################
	
	/**
	 * Returns the number of distinct elements in this HashEList
	 * @return the number of elements
	 */
	public int size() {
		return originEList.size();
	}

	/**
	 * Returns true if this HashEList contains no elements 
	 * @return whether this HashEList contains no elements 
	 */
	public boolean isEmpty() {
		return originEList.isEmpty();
	}
	
	/**
	 * Returns an iterator over the distinct elements in this HashEList in proper sequence.
	 * @return an iterator over the distinct elements in this HashEList in proper sequence.
	 */
	public Iterator<E> iterator() {
		return originEList.iterator();
	}

	/**
	 * Returns an array containing all of the distinct elements in this HashEList in proper sequence
	 * @return an array containing all of the distinct elements in this HashEList in proper sequence
	 */
	public Object[] toArray() {
		return originEList.toArray();
	}

	/**
	 * Returns an array containing all of the dictinct elements in this HashEList in proper sequence; the runtime type of the returned array is that of the given array
	 * @param a the array into which the distinct elements of this HashEList are to be stored    
	 */
	public <T> T[] toArray(T[] a) {
		return originEList.toArray(a);
	}

	/**
	 * Returns the element at the given index in this HashEList 
	 * @param index the index of element to get
	 * @return the element at the given index in this HashEList
	 */
	public E get(int index) {
		return originEList.get(index);
	}
	
	/**
	 * Returns the index of the first occurrence of the given element in this HashEList, or -1 if it does not contain the element
	 * @param o the object to search for
	 * @return the index of the object, or -1 
	 */
	public int indexOf(Object o) {
		return originEList.indexOf(o);
	}

	/**
	 * Returns the index of the last occurrence of the given element in this HashEList, or -1 if it does not contain the element
	 * @param o the object to search for
	 * @return the index of the object, or -1 
	 */
	public int lastIndexOf(Object o) {
		return originEList.lastIndexOf(o);
	}

	/**
	 * Returns a list iterator over the elements in this HashEList in proper sequence
	 * @return a list iterator over the elements in this HashEList in proper sequence
	 */
	public ListIterator<E> listIterator() {
		return originEList.listIterator();
	}

	/**
     * Returns a list iterator over the elements in this HashEList in proper sequence, starting at the given index
	 * @param index the index at which to start
	 * @return a list iterator over the elements in this HashEList in proper sequence, starting at the given index
	 */
	public ListIterator<E> listIterator(int index) {
		return originEList.listIterator(index);
	}

	/**
	 * Returns a view of the portion of this list between the specified fromIndex, inclusive, and toIndex, exclusive
	 * @param fromIndex low endpoint (inclusive) of the subList
	 * @param toIndex high endpoint (exclusive) of the subList
	 * @return
	 */
	public List<E> subList(int fromIndex, int toIndex) {
		return originEList.subList(fromIndex, toIndex);
	}

	/**
	 * Moves the given object to the given position, if is in this HashEList
	 * @param newPosition the position of the object after the move
	 * @param E the object to move 
	 */
	public void move(int newPosition, E object) {
		originEList.move(newPosition, object);
	}

	/**
	 * Moves the object at the given old position to the given new position
     * @param oldPosition the position of the object before the move
	 * @param newPosition the position of the object after the move
	 * @return the moved object
     *   
	 */
	public E move(int newPosition, int oldPosition) {
		return originEList.move(newPosition, oldPosition);
	}

}
